Laboratorio de Riesgos
Agenda:
Introducción a la programación en Python
Python
Valoración de derivados financieros con Python
Modelo multifactorial de riesgo de crédito
Un lenguaje de programación es un conjunto formal de instrucciones que permite a los humanos comunicarse con las computadoras para resolver problemas mediante algoritmos.
Un lenguaje de programación actúa como un puente entre el pensamiento humano y la lógica de la máquina. Consiste en vocabulario (palabras reservadas como if, for), gramática (reglas de sintaxis) y semántica (significado de las instrucciones).
Las computadoras solo entienden lenguaje máquina (binario: 0s y 1s), por lo que los compiladores (programa queda listo para ejecutarse directamente en un procesador específico) o intérpretes (Traducción dinámica en tiempo de ejecución (Just-In-Time, JIT)) traducen el código fuente a instrucciones ejecutables.
Características principales de cualquier lenguaje de programación:
Python es un lenguaje de programación de alto nivel interpretado, multiparadigma y de propósito general creado a finales de los años 80 por Guido van Rossum. Se caracteriza por su énfasis en la legibilidad del código y la simplicidad de la sintaxis, convirtiendolo en una opción ideal tanto para principiantes como para desarrolladores experimentados.
La filosofía de diseño de Python se resume en el “Zen de Python” (PEP 20), es un conjunto de 19 principios que guían el diseño del lenguaje Python y promueven la escritura de código limpio, legible y eficiente.
Pueden consultarse ejecutando el siguiente comando en cualquier intérprete Python:
No osbstante, destacamos los siguientes:
Deuda ténica
Metáfora usada en desarrollo de software para describir los costes futuros por tomar atajos o decisiones subóptimas (código sucio, mal estructurado,…) con el objetivo de acortar tiempos o entregar mas rápido.
Similar a una deuda financiera que acumula intereses, dificultando el mantenimiento y desarrollo futuro.
| Aspecto | Python | R | C++ | Java | Rust |
|---|---|---|---|---|---|
| % de uso1 | 57,9% | 4,9% | 23,5% | 29.3% | 14,8% |
| Tipo de Lenguaje | Multiparadigma | Funcional | Multiparadigma | Orientado a Objetos | Multiparadigma |
| Compilado/Interpretado | Interpretado | Interpretado | Compilado | Compilado (bytecode) | Compilado |
| Gestión de la memoria | Automatico (GC) | Automatico (GC) | Manual | Automatico (GC) | Compilado (Ownership) |
| Tipado | Dinámico | Dinámico | Estático | Estático | Estático |
| Curva de Aprendizaje | Medio | Medio | Muy Alta | Alta | Muy Alta |
| Velocidad Ejecución | Medio | Medio | Muy Rápido | Rápido | Muy Rápido |
| Velocidad Desarrollo | Muy Rápido | Rápido | Lento | Lento | Lento |
| Año de Creación | 1991 | 1995 | 1983 | 1995 | 2010 |
Como Musashi (2018) enseña en El Libro de los Cinco Anillos, no existe el arma perfecta universal: cada una (espada, lanza, arco,…) brilla en su contexto específico. La maestría radica en adaptarse a las circunstancias y elegir la herramienta adecuada para cada situación.
| Ventaja | Impacto |
|---|---|
| Legibilidad | Código fácil de enteder |
| Prototipado rápido | Idea → Código en horas, no meses |
| Librerias disponibles | Amplio abanico de utilidades ya escritas |
| Gratuito | Sin coste |
| Producción | De prototipos a producción |
| Proposito general | Soluciones para casi cualquier problema |
| Desventaja | Severidad |
|---|---|
| Velocidad | Bucles lentos vs. C++ (1000x más lento) |
| Memoria | Mayor consumo para datasets masivos |
| Latencia | No apto para trading HFT (microsegundos) |
| GIL (Global Interpreter Lock) | Multithread limitado en CPU |
| Producción crítica | Necesita testing exhaustivo |
“If you only have a hammer, everything looks like a nail”
El componente central para programar en Python es el intérprete, que es el software que traduce y ejecuta tu código. Lo reconoceremos por los siguientes símbolos “>>>” o “…”
Instalar el interprete de Python
En Windows: Descarga el instalador ejecutable (.exe) a través de la página oficial
Durante la instalación, es importante añadir el directorio de instalación al “PATH”, lo que permite ejecutar Python desde cualquier terminal sin especificar la ruta completa. Una vez completada la instalación, verifica escribiendo en la línea de comandos:
Un editor de código o IDE es la herramienta donde escribirás tu código Python. Las opciones varían según tu nivel y necesidades:
Instalar Positron
Existen otros: Visual Studio, Jupiter, Spyder
pip (Package Installer for Python) es la herramienta estándar para instalar librerías y paquetes adicionales. Viene incluido automáticamente con Python
Al ejecutar el comando pip install el gestor de paquetes pip buscará en los repositorios públicos, si existe lo descargará, junto a sus dependencias.
Agenda:
Introducción a la programación en Python
Python
Valoración de derivados financieros con Python
Modelo multifactorial de riesgo de crédito
Programa que lee, traduce y ejecuta el código línea por línea en tiempo real, actuando como puente entre tu lógica y la máquina
También conocido como REPL (Read-Eval-Print Loop), opera en un ciclo continuo: lee tu código, lo evalúa, imprime el resultado y espera la siguiente instrucción. Cuando ejecutas python en la terminal, inicia una sesión interactiva que muestra el prompt primario >>> para comandos nuevos y el prompt secundario ... para líneas continuas, como bucles o funciones.
$ python
Python 3.12.3 (main, Jun 18 2025, 17:59:45) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> quit()El intérprete muestra información de versión y copyright, luego el prompt >>>. Para salir, usa: quit(), exit() o Ctrl+C (Linux/macOS) / Ctrl+Z (Windows).
>>>.Durante la evaluación el intérprete ignora los denominados comentarios, son anotaciones en el código para documentar, explicar lógica o desactivar temporalmente líneas sin generar errores. Se crean con el símbolo # al inicio de una línea o después del código. Para bloques multilínea, se usan comillas triples (’’’ o “““) sin asignar a variable
# Esto es un comentario ignorado por Python
print("Hola Mundo") # A partir de '#' el interprete no lo procesa
'''
Este es un comentario multinea:
Los comentarios mejoran la legibilidad y mantenimiento del código.
Las buenas prácticas recomiendan comentarios concisos, actualizados y enfocados en el "porqué" más que en el "qué".
'''El intérprete actua como una simple calculadora. Por ejemplo:
2 + 2
50 - 5 * 6
(50 - 5 * 6) / 4
5 ** 2
17 / 3 # division clasiva
17 // 3 # operador //: cociente sin la parte decimal
17 % 3 # operador %: modulo
5 * 3 + 2 # cociente * divisor + resto
width = 20
height = 5 * 9
width * height
x, y = 10, 20 # Asignación múltiple
a = b = c = 0 # Dar mismo valor a varias variablesPodemos ejecutar cualquier operación (+, -,*, /) y nos devolverá el resultado. Además podemos usar paréntesis (()) para agrupar operaciones
\[ \begin{align} 2 + 2 &= 4 \\ 50 - 5 · 6 &= 20 \\ \frac{(50 - 5 · 6)}{4} &= 5 \\ \end{align} \]
Podemo usar el operador ** para calcular potencias, por ejemplo:
\[ 5^2 = 25 \]
La divisíon cĺasica /, el cociente entero //, operador módulo %:
\[ \begin{align} \frac{17}{3} &= 5.666666666666667 \\ 17 \lfloor 3 &= 5 \\ 17 \pmod{3} &= 2 \\ 5 · 3 + 2 &= 17 \end{align} \]
El signo igual (=) se usa para asignar un valor a una variable. Cuando asignamos una o múltiples variables, el interprete no mostrará ningun resultado, antes del siguiente prompt interactivo:
No obstante, existen 35 términos predefinidos que el intérprete reserva exclusivamente para su sintaxis y control de flujo, impidiendo su uso como nombres de variables, funciones o identificadores para evitar conflictos. Pueden ser consultadas ejecutando el siguiente comando:
Hay tres operadores lógicos en Python: and, or y not, sirven para evaluar expresiones booleanas y permiten combinar condiciones en estructuras de control como if o bucles.
and (&)Devuelve True solo si ambos operandos son verdaderos, evaluación en cortocircuito. Ejemplo: True and False resulta en False.
or (|)Retorna True si al menos un operando es verdadero, también con cortocircuito. Ejemplo: True or False devuelve True.
not (!)Invierte el valor booleano del operando: not True es False, y viceversa; tiene mayor precedencia que and y or (orden: not > and > or). Los operadores de comparación en Python, como “greater than” (>), complementan a los lógicos al generar valores booleanos para condiciones.
Se integran con and, or y not en expresiones como if x > 10 and y <= 20:, evaluando precedencia de izquierda a derecha con cortocircuito.
Como dijimos anteriormente, Python es un lenguaje dinamicante tipado, el intérprete se encarga de inferir el tipo del objeto en tiempo de ejecución. Al contrario que en los lenguajes compilados como C++ o Rust en los que generalemente su tipado es estático (definido por el desarrollador) o inferido en tiempo de compilación. Ver (Python Software Foundation, 2025) para tipos de datos mas espcializados.
| Tipo | Significado | Uso |
|---|---|---|
| int | Integer | Números naturales |
| float | Número con punto flotante | Números reales |
| bool | Booleano | Valores binarios (Verdadero/Falso) |
| str | String | Caracteres, palabras, texto, … |
Integer (int) - representa números enteros, valores numéricos sin parte decimal, pueden ser positivos o negativos. En Python 3 los enteros tienen precisión arbitraria; no tienen un límite de tamaño fijo (overflow).
for, while) y acceder a posiciones en listas o bases de datos.Como dijimos anteriormente, Python es un lenguaje dinamicante tipado, el intérprete se encarga de inferir el tipo del objeto en tiempo de ejecución. Al contrario que en los lenguajes compilados como C++ o Rust en los que generalemente su tipado es estático (definido por el desarrollador) o inferido en tiempo de compilación. Ver (Python Software Foundation, 2025) para tipos de datos mas espcializados.
| Tipo | Significado | Uso |
|---|---|---|
| int | Integer | Números naturales |
| float | Número con punto flotante | Números reales |
| bool | Booleano | Valores binarios (Verdadero/Falso) |
| str | String | Caracteres, palabras, texto, … |
Número con punto flotante (float) - se utiliza para representar números reales con parte decimal. En Python, estos se implementan generalmente como valores de “doble precisión”.
Como dijimos anteriormente, Python es un lenguaje dinamicante tipado, el intérprete se encarga de inferir el tipo del objeto en tiempo de ejecución. Al contrario que en los lenguajes compilados como C++ o Rust en los que generalemente su tipado es estático (definido por el desarrollador) o inferido en tiempo de compilación. Ver (Python Software Foundation, 2025) para tipos de datos mas espcializados.
| Tipo | Significado | Uso |
|---|---|---|
| int | Integer | Números naturales |
| float | Número con punto flotante | Números reales |
| bool | Booleano | Valores binarios (Verdadero/Falso) |
| str | String | Caracteres, palabras, texto, … |
Booleano (bool) - solo puede tomar dos valores: True (Verdadero) o False (Falso). Es una subclase de int en Python (donde True se comporta como 1 y False como 0 en contextos aritméticos).
is_active, has_error, market_open).precio > 100 devuelve un bool).Como dijimos anteriormente, Python es un lenguaje dinamicante tipado, el intérprete se encarga de inferir el tipo del objeto en tiempo de ejecución. Al contrario que en los lenguajes compilados como C++ o Rust en los que generalemente su tipado es estático (definido por el desarrollador) o inferido en tiempo de compilación. Ver (Python Software Foundation, 2025) para tipos de datos mas espcializados.
| Tipo | Significado | Uso |
|---|---|---|
| int | Integer | Números naturales |
| float | Número con punto flotante | Números reales |
| bool | Booleano | Valores binarios (Verdadero/Falso) |
| str | String | Caracteres, palabras, texto, … |
String (str) - Es una secuencia inmutable de caracteres. Se delimitan con comillas simples (’ ’) o dobles (” “). Al ser inmutables, no se pueden modificar en el mismo espacio de memoria una vez creados; cualquier”cambio” genera una nueva cadena.
None es el objeto singleton que utiliza Python para representar la ausencia de valor o un estado nulo. Se usa para inicializar variables vacías o como valor por defecto en funciones. Se compara con is no con ==
type()La función type(obj) devuelve la clase exacta del objeto obj
Usamos los constructores de clase para convertir entre tipos: int(), float(), str(), list().
Python es fuertemente tipado pero dinámico. No hace “casting implícito” que pierda datos (como sumar “5” + 5 dará error), por lo que debemos convertir explícitamente.
Son estructuras de datos que permiten almacenar y organizar múltiples elementos, agrupándolos en tipos especializados. Todas admiten elementos heterogéneos, son iterables e indexables (excepto sets), y ofrecen métodos optimizados para agregar, eliminar o buscar datos según su naturaleza mutable/inmutable. Ver (Python Software Foundation, 2025).
| Tipo | Mutable / Inmutable | Uso |
|---|---|---|
| list | Mutable | Conjunto variable de objetos |
| tuple | Inmutable | Conjunto fijo de objetos |
| dict | Mutable | Almacenamiento clave-valor |
| set | Mutable | Conjunto: Colección de objetos únicos |
Lista (list) - Secuencias mutables y ordenadas, definidas con [] o list(), usadas para datos modificables. Pueden contener objetos de distintos tipos (enteros, strings, incluso otras listas) mezclados.
Son estructuras de datos que permiten almacenar y organizar múltiples elementos, agrupándolos en tipos especializados. Todas admiten elementos heterogéneos, son iterables e indexables (excepto sets), y ofrecen métodos optimizados para agregar, eliminar o buscar datos según su naturaleza mutable/inmutable. Ver (Python Software Foundation, 2025).
| Tipo | Mutable / Inmutable | Uso |
|---|---|---|
| list | Mutable | Conjunto variable de objetos |
| tuple | Inmutable | Conjunto fijo de objetos |
| dict | Mutable | Almacenamiento clave-valor |
| set | Mutable | Conjunto: Colección de objetos únicos |
Tupla (tuple) - Secuencias inmutables y ordenadas, definidas con () o tuple(). Una vez creada, no puedes añadir, borrar ni cambiar sus elementos. Esto las hace más ligeras en memoria y seguras contra modificaciones accidentales.
Son estructuras de datos que permiten almacenar y organizar múltiples elementos, agrupándolos en tipos especializados. Todas admiten elementos heterogéneos, son iterables e indexables (excepto sets), y ofrecen métodos optimizados para agregar, eliminar o buscar datos según su naturaleza mutable/inmutable. Ver (Python Software Foundation, 2025).
| Tipo | Mutable / Inmutable | Uso |
|---|---|---|
| list | Mutable | Conjunto variable de objetos |
| tuple | Inmutable | Conjunto fijo de objetos |
| dict | Mutable | Almacenamiento clave-valor |
| set | Mutable | Conjunto: Colección de objetos únicos |
Diccionario (dict) - Pares clave-valor no ordenados, como {"nombre": "Ana", "edad": 25}, definidas como: {clave: valor} o dict():
Son estructuras de datos que permiten almacenar y organizar múltiples elementos, agrupándolos en tipos especializados. Todas admiten elementos heterogéneos, son iterables e indexables (excepto sets), y ofrecen métodos optimizados para agregar, eliminar o buscar datos según su naturaleza mutable/inmutable. Ver (Python Software Foundation, 2025).
| Tipo | Mutable / Inmutable | Uso |
|---|---|---|
| list | Mutable | Conjunto variable de objetos |
| tuple | Inmutable | Conjunto fijo de objetos |
| dict | Mutable | Almacenamiento clave-valor |
| set | Mutable | Conjunto: colección de objetos únicos |
Conjuntos (set) - Estructuras de datos no ordenadas sin duplicados ni indexación, definidas con {valor1, valor2} o set(), útiles para operaciones únicas.
if x in mi_set).Los loops (bucles) son estructuras de control que permiten ejecutar un bloque de código múltiples veces, iterando sobre secuencias o bajo condiciones específicas los principales son: for y while.
forPermite especificar de antemano el número de iteraciones. Se define con for seguido de la variable receptora, la palabra in, la secuencia iterable, dos puntos : y el cuerpo indentado del bloque a ejecutar.
# Iterar sobre lista
frutas = ["manzana", "plátano", "naranja"]
for it_fruta in frutas:
print(it_fruta)
# Iterar sobre rango
for it_range in range(5): # 0, 1, 2, 3, 4
print(f"Número: {it_range}")
# Iterar sobre string
for it_letra in "Python":
print(it_letra)
# Iterar con índice
for it_indice, it_fruta in enumerate(frutas):
print(f"{it_indice}: {it_fruta}")
# Iterar sobre múltiples secuencias simultáneamente
nombres = ["Ana", "Bob", "Carlos"]
edades = [25, 30, 35]
for it_nombre, it_edad in zip(nombres, edades):
print(f"{nombre} tiene {edad} años")# Crear lista de cuadrados
[x**2 for x in range(5)] # [0, 1, 4, 9, 16]
# Con condición
[x for x in range(10) if x % 2 == 0] # [0, 2, 4, 6, 8]
# Comprensión de diccionario
{x: x**2 for x in range(4)} # {0: 0, 1: 1, 2: 4, 3: 9}
# Iterar sobre diccionarios
persona = {"nombre": "Ana", "edad": 25, "ciudad": "Madrid"}
# Iterar sobre claves
for it_clave in persona:
print(it_clave)
# Iterar sobre valores
for it_valor in persona.values():
print(it_valor)
# Iterar sobre pares clave-valor
for it_clave, it_valor in persona.items():
print(f"{it_clave}: {it_valor}")Los loops (bucles) son estructuras de control que permiten ejecutar un bloque de código múltiples veces, iterando sobre secuencias o bajo condiciones específicas los principales son: for y while.
whilePermite ejecutar un bloque de código repetidamente mientras se cumpla una condición específica. Se define con while seguido de la condición booleana, dos puntos : y el cuerpo indentado que se repite hasta que la condición sea falsa.
break: Termina el loop prematuramente:
continue: Salta la iteración actual y continúa con la siguiente:
else: Se ejecuta cuando el loop termina sin break:
Las funciones son bloques de código que pueden ser reutilizados y que realizan una tarea específica, pueden recibir parámetros como entrada y pueden devolver resultados. Las ventajas de usar funciones son: reutilización (escribir código una sola vez, usarlo muchas veces), modularidad (dividir problemas complejos en tareas simples), mantenibilidad (cambios centralizados en una ubicación) y legibilidad (código más organizado y fácil de entender).
Una función se define con la palabra reservada def seguida del nombre de la función, paréntesis con parámetros opcionales y dos puntos, iniciando un bloque indentado con la lógica a ejecutar.
Componentes:
Otra forma de definirlas es a través de funciones anónimas: funciones sin nombre que se define en una sola línea usando la palabra reservada lambda.
Los parámetros de una función son variables que reciben valores (argumentos) cuando se invoca, permitiendo que la función procese datos de entrada y realice operaciones específicas sobre ellos.
Tipos de parámetros
def saludar(nombre, mensaje="Hola"):
return f"{mensaje}, {nombre}"
print(saludar("Ana")) # Posicional + valor por defecto - Output: Hola, Ana
print(saludar("Ana", "Buenos días")) # Posicional sin valor por defecto - Output: Buenos días, Ana
print(saludar(mensaje="Buenos días", nombre="Ana")) # Nombradosnumero_e = 2.718281828459045
def calcular_valor_futuro(capital_inicial, tasa, t, tipo_capitalizacion):
if tipo_capitalizacion == "simple":
return capital_inicial * (1 + tasa * t)
elif tipo_capitalizacion == "compuesto":
return capital_inicial * (1 + tasa) ** t
elif tipo_capitalizacion == "continuo":
# Alternativamente math.exp() para e^(x) importando math: import math
return capital_inicial * (numero_e ** (tasa * t))
else:
print("Opción no válida.")
return
calcular_valor_futuro(100, 0.01, 5, "simple")
calcular_valor_futuro(100, 0.01, 5, "compuesto")
calcular_valor_futuro(100, 0.01, 5, "continuo")Escribir una funcion que genere el numero \(i\) de la serie de Fibonacci.
En la serie de Fibonacci el número \(i\) es la número es la suma de los dos anteriores.
La secuencia comienza con los números 0 y 1. Es decir, \(F(0) = 0\) y \(F(1) = 1\)
A partir de ahí, el siguiente número se calcula sumando los dos anteriores:
\(F_i = F_{i−1} + F_{i−2}\)
Serie de Fibonnaci: \(0, 1, 1, 2, 3, 5, 8, 13, 21, 34...\)
Escribir una funcion que genere el numero \(i\) de la serie de Fibonacci.
En la serie de Fibonacci el número \(i\) es la número es la suma de los dos anteriores.
La secuencia comienza con los números 0 y 1. Es decir, \(F(0) = 0\) y \(F(1) = 1\)
A partir de ahí, el siguiente número se calcula sumando los dos anteriores:
\(F_i = F_{i−1} + F_{i−2}\)
Serie de Fibonnaci: \(0, 1, 1, 2, 3, 5, 8, 13, 21, 34...\)
# Opcion a:
def fibonacci(i):
if i == 0: return 0
if i == 1: return 1
a, b = 0, 1
for it in range(2, i + 1):
a = b
b = a + b
return b
# Opcion b:
def fibonacci(i):
if i == 0: return 0
if i == 1: return 1
resultado = fibonacci(i - 1) + fibonacci(i - 2)
return resultado
for it in range(0, 100):
print(fibonacci(it))Aunque la definición matemática es recursiva (\(F(i)=F(i−1)+F(i−2)\)), programarla con un bucle (for) tiene complejidad lineal \(O(n)\).
Si usaramos recursión simple:
return fibonacci(i-1) + fibonacci(i-2)
la complejidad es exponencial, haciendo que calcular el término 50 tarde mucho tiempo, ya que reevaluamos lo evaluado.
Escribir una funcion que genere el numero \(i\) de la serie de Fibonacci.
En la serie de Fibonacci el número \(i\) es la número es la suma de los dos anteriores.
La secuencia comienza con los números 0 y 1. Es decir, \(F(0) = 0\) y \(F(1) = 1\)
A partir de ahí, el siguiente número se calcula sumando los dos anteriores:
\(F_i = F_{i−1} + F_{i−2}\)
Serie de Fibonnaci: \(0, 1, 1, 2, 3, 5, 8, 13, 21, 34...\)
def fibonacci(i, cache = {0: 0, 1: 1}):
if i in cache:
return cache[i]
resultado = fibonacci(i - 1, cache) + fibonacci(i - 2, cache)
cache[i] = resultado
return resultado
for it in range(0, 100):
print(fibonacci(it))Esta técnica se conoce técnicamente como Memoización (Memoization). Al almacenar los resultados previos, transformamos un algoritmo de complejidad exponencial \(O(2^n)\) a uno lineal \(O(n)\), haciendo viable la recursividad para números grandes.
Escribe un programa en Python que calcule numericamente la integral definida de la función \(f(x) = x ^ 2 · sin(x)\) en el intervalo [a, b]:
\[ \int^a_b x^2 · sin(x) dx \]
En muchos problemas de finanzas, nos encontramos con funciones que son difíciles o imposibles de integrar analíticamente (con lápiz y papel). En estos casos, aproximamos el área bajo la curva dividiendo el intervalo en pequeños rectángulos (Suma de Riemann). El ancho de todas las barras sera igual mientras que a altura de cada barra la determina el valor de la función en ese punto.
integral_riemann(f, a, b, n)Algoritmo (Suma del area de rectángulos):
n veces. En cada paso:x del lado izquierdo del rectangulo: \(x_i=a+i·\Delta x\).import math
def f(x):
return (x**2) * math.sin(x)
def integral_izquierda(a, b, n):
"""
Calcula la integral usando Suma de Riemann (Extremo Izquierdo).
La altura del rectángulo se define por el valor de f(x)
al inicio (izquierda) de cada sub-intervalo.
"""
ancho = (b - a) / n
suma_areas = 0.0
for i in range(n):
# Usamos el borde izquierdo del rectángulo 'i'
x = a + ancho * i
altura = f(x)
suma_areas += ancho * altura
return suma_areas
a = 0
b = math.pi
valor_real = 5.869604401 # (pi^2 - 4)
integral_izquierda(a, b, 10)
integral_izquierda(a, b, 100)
integral_izquierda(a, b, 1000)
integral_izquierda(a, b, 1000000)Las librerías y módulos son colecciones de código reutilizable que agrupan funciones, clases y variables relacionadas, permitiendo extender la funcionalidad del lenguaje sin reescribir código desde cero.
Archivo .py que contiene código Python (funciones, clases, variables) que puede importarse a través de la palabra reservado import y reutilizarse en otros programas:
Colección organizada de módulos en directorios con estructura jerárquica, facilitando la gestión de proyectos grandes.
# Importar Módulo Completo
import matematica
resultado = matematica.sumar(5, 3) # Output: 8
# Importar Funciones Específicas
from matematica import sumar, PI
resultado = sumar(10, 5) # Output: 15
print(PI) # Output: 3.14159
# Importar con Alias
import matematica as mat
from mat import sumar as suma_numeros
resultado = suma_numeros(7, 2)
# Importar todo
import * from matematica
resultado = sumar(7, 2)Operaciones matemáticas como raíces cuadradas (sqrt()), funciones trigonométricas (sin(), cos(), tan()), logaritmos, exponenciales y constantes matemáticas fundamentales como pi y e, además de funciones para números enteros como factorial() y gcd().
Genera números pseudoaleatorios, incluyendo enteros (randint()), flotantes uniformes (random()), selección aleatoria de elementos (choice()), muestreo (sample()) y mezclado de listas (shuffle()).
Fechas, horas y duraciones, permitiendo crear objetos de fecha/hora (datetime.now()), calcular diferencias (timedelta), formatear fechas (strftime()), instanciar fechas a partir de strings (strptime()) y realizar operaciones de calendario.
from datetime import datetime, timedelta
ahora = datetime.now()
print(ahora)
mañana = ahora + timedelta(days=1)Ofrece interacción con el sistema operativo para operaciones de archivos y directorios como obtener el directorio actual (getcwd()), listar contenido (listdir()), crear/eliminar carpetas (mkdir(), rmdir()) y navegar rutas (path.join()).
Las clases en Python son plantillas para crear objetos con el objetivo de extender los tipos básicos que contienen atributos (datos) y métodos (funciones). Es un paradigma de programación que permite encapsular y representar nuestro problemas en unidades lógicas.
Cuando definimos una clase, el término self hace referencia al objeto actual, permitiendo acceder a sus atributos y métodos sin estar instanciado.
class Persona:
"""Clase que representa una persona"""
def __init__(self, nombre, edad):
"""Constructor: inicializa atributos"""
self.nombre = nombre
self.edad = edad
def saludar(self, apellido = ""):
"""Método: función dentro de la clase"""
return f"Hola, soy {self.nombre} {apellido}"
# Crear objeto (instancia)
persona1 = Persona("Ana", 25)
print(persona1.saludar("Martinez"))
# Output: Hola, soy Ana MartinezUna clase se define con la palabra reservada class seguido del nombre, :, y a continuación se declaran los atributos y métodos usando la indentación:
Variables que pertenecen a la clase, almacenando datos del objeto:
Podemos acceder a sus atributos a través de self.nombre desde dentro de la clase o usando . despues del objeto instanciado objeto.nombre desde fuera de la clase.
Son funciones que “viven” dentro de la clase. Permite que los objetos no solo almacenen datos (atributos), sino que también hagan cosas con ellos.
De igual manera que los atributos, podemos acceder a sus atributos a través de self.saludar() desde dentro de la clase o usando . despues del objeto instanciado objeto.saludar() desde fuera de la clase.
El constructor es un método especial que se ejecuta al crear una instancia, inicializando atributos.
Se declara usando def __init__():. Y define como se va a instanciar el objeto y los parametros que necesita.
Implementar una clase que represente una única exposición crediticia y encapsule la lógica para calcular la pérdida esperada y capital regulatorio (fórmula de Vasicek).
Inicializar los 4 parámetros de riesgo como atributos del objeto.:
ead: Exposición en Default.pd: Probabilidad de incumplimiento.lgd: Severidad.rho: Correlación de activos.Métodos:
\[ \text{Perdida Esperada} = EAD · LGD · PD \]
\[ Capital = LGD · \left( \Phi \left(\frac{\Phi^{-1}(PD) + \sqrt\rho \Phi^{-1}(\alpha)}{\sqrt{1 - \rho}} \right) - PD\right) \]
donde: \(\alpha\) es el nivel de confianza (por defecto, 99.9%), \(\Phi\) es la distribución normal estándar acumulada norm.cdf) de la libreria from scipy.stats import norm y \(\Phi^{-1}\) es la inversa de la normal estándar norm.cdf.
from scipy.stats import norm
import math
class Posicion:
def __init__(self, ead, pd, lgd, rho):
self.ead = float(ead)
self.pd = float(pd)
self.lgd = float(lgd)
self.rho = float(rho)
def calcular_el(self):
return self.ead * self.pd * self.lgd
def calcular_capital(self, confianza=0.999):
if self.pd == 0 or self.pd == 1 or self.rho == 1:
return 0.0
# Componentes de la fórmula de Vasicek
inv_pd = norm.ppf(self.pd) # N^-1(PD)
inv_conf = norm.ppf(confianza) # N^-1(0.999)
numerador = inv_pd + (math.sqrt(self.rho) * inv_conf)
denominador = math.sqrt(1 - self.rho)
# PD condicional
pd_cond = float(norm.cdf(numerador / denominador))
# Capital Rate = LGD * (PD_cond - PD_uncond)
k_rate = self.lgd * (pd_cond - self.pd)
return self.ead * k_rateImplementar la clase de un bono cupón cero
Inicializar dos atributos:
nominal: El valor monetario del bono al vencimiento.maturity_date: Un objeto de tipo datetime que representa la fecha de vencimiento.Valor del bono recibe:
rate: La tasa de interés anual de mercado.valuation_date: (Opcional) La fecha en la que se quiere valorar el bono.\[ NPV = \frac{Nominal}{(1 + rate) ^ {\frac{\text{maturity date} - \text{valuation date}}{365} }} \]
from datetime import datetime
import math
class ZeroCouponBond:
def __init__(self, nominal, maturity_date):
self.nominal = float(nominal)
self.maturity = maturity_date
def npv(self, rate, valuation_date=None):
# 1. Determinar fecha de valoración
if valuation_date is None:
val_date = datetime.now()
if isinstance(valuation_date, datetime):
val_date = valuation_date
else:
return 0
# si ya venció, el valor presente es 0 (o ya se pagó)
if val_date >= self.maturity:
return 0.0
dias_a_vencimiento = (self.maturity - val_date).days
T = dias_a_vencimiento / 365.0
# NPV = N / (1 + r)^(T)
return self.nominal / ((1 + rate) ** T)La herencia es un mecanismo que permite definir una nueva clase a partir de otra existente, de forma que la clase derivada reutiliza, amplía o modifica los atributos y métodos de la clase base. La idea es modelar una relación “es-un” (por ejemplo, Coche es un Vehículo), donde la clase más general aporta el comportamiento común y las clases más específicas añaden o especializan funcionalidades, reduciendo duplicación de código y creando jerarquías más claras y mantenibles.
class Animal:
"""Clase base (superclase)"""
def __init__(self, nombre):
self.nombre = nombre
def hacer_sonido(self):
return "Sonido genérico"
class Perro(Animal):
"""Clase derivada (subclase) hereda de Animal"""
pass
# Uso
perro = Perro("Max")
print(perro.nombre)
# Output: Max (heredado)
print(perro.hacer_sonido())
# Output: Sonido genérico (heredado)La herencia se define colocando entre paréntesis el nombre de la clase padre inmediatamente después del nombre de la clase hija en la declaración de la clase.
La subclase automáticamente recibe todos los métodos y atributos de la clase padre, pudiendo sobreescribirlos o extenderlos según sea necesario.
La herencia es un mecanismo que permite definir una nueva clase a partir de otra existente, de forma que la clase derivada reutiliza, amplía o modifica los atributos y métodos de la clase base. La idea es modelar una relación “es-un” (por ejemplo, Coche es un Vehículo), donde la clase más general aporta el comportamiento común y las clases más específicas añaden o especializan funcionalidades, reduciendo duplicación de código y creando jerarquías más claras y mantenibles.
class Animal:
"""Clase base (superclase)"""
def __init__(self, nombre):
self.nombre = nombre
def hacer_sonido(self):
return "Sonido genérico"
class Perro(Animal):
def hacer_sonido(self):
return "¡Guau! ¡Guau!"
class Gato(Animal):
def hacer_sonido(self):
return "Miau miau"
# Uso
perro = Perro("Rex")
gato = Gato("Whiskers")
print(perro.hacer_sonido())
# Output: ¡Guau! ¡Guau!
print(gato.hacer_sonido())
# Output: Miau miauLa herencia se define colocando entre paréntesis el nombre de la clase base inmediatamente después del nombre de la clase derivada en la declaración de la clase.
La subclase automáticamente recibe todos los métodos y atributos de la clase base, pudiendo sobreescribirlos o extenderlos según sea necesario.
La herencia es un mecanismo que permite definir una nueva clase a partir de otra existente, de forma que la clase derivada reutiliza, amplía o modifica los atributos y métodos de la clase base. La idea es modelar una relación “es-un” (por ejemplo, Coche es un Vehículo), donde la clase más general aporta el comportamiento común y las clases más específicas añaden o especializan funcionalidades, reduciendo duplicación de código y creando jerarquías más claras y mantenibles.
class Animal:
"""Clase base (superclase)"""
def __init__(self, nombre):
self.nombre = nombre
def hacer_sonido(self):
return "Sonido genérico"
class Perro(Animal):
def __init__(self, nombre, raza):
# Llama al constructor de Animal
Animal.__init__(nombre)
# super().__init__(nombre) # Llama constructor padre
self.raza = raza
def hacer_sonido(self):
return "¡Guau! ¡Guau!"
class Gato(Animal):
def hacer_sonido(self):
return "Miau miau"
# Uso
perro = Perro("Rex", "Golden Retriever")
gato = Gato("Whiskers")
print(perro.hacer_sonido())
# Output: ¡Guau! ¡Guau!
print(gato.hacer_sonido())
# Output: Miau miauUsar super() o el nombre de la clase para llamar al constructor de la clase padre:
Además, podemos usar isintance() y issubclass() para evaluar el tipo de la clase y sus herencias:
from typing import Union
class Punto2D:
"""
Representa un punto en el plano cartesiano 2D.
"""
def __init__(self, x=0, y=0) -> None:
"""Constructor: crea punto con coordenadas x, y"""
self.x = float(x)
self.y = float(y)
def __repr__(self) -> str:
"""Representación oficial para debug/reconstrucción"""
return f'Punto2D({self.x}, {self.y})'
def __str__(self) -> str:
"""Representación legible para usuario final"""
return f'({self.x}, {self.y})'
def __eq__(self, other) -> bool:
"""Comparación de igualdad"""
if not isinstance(other, Punto2D):
return NotImplemented
return self.x == other.x and self.y == other.y
def __lt__(self, other) -> bool:
"""Menor que: orden lexicográfico (primero x, luego y)"""
if not isinstance(other, Punto2D):
return NotImplemented
return (self.x, self.y) < (other.x, other.y)
def __len__(self) -> int:
"""Número de dimensiones (siempre 2)"""
return 2
def __bool__(self) -> bool:
"""Verdadero si no es el origen (0,0)"""
return self.x != 0 or self.y != 0
def __add__(self, other: "Point2D") -> "Point2D":
"""Suma vectorial: p1 + p2"""
if isinstance(other, Punto2D):
return Punto2D(self.x + other.x, self.y + other.y)
return NotImplemented
def __sub__(self, other: "Point2D") -> "Point2D":
"""Resta vectorial: p1 - p2"""
if isinstance(other, Punto2D):
return Punto2D(self.x - other.x, self.y - other.y)
return NotImplemented
def __mul__(self, escalar: Union[int, float]) -> "Point2D":
"""Multiplicación por escalar"""
if isinstance(escalar, (int, float)):
return Punto2D(self.x * escalar, self.y * escalar)
return NotImplemented
def __getitem__(self, indice):
"""Acceso por índice: p[0] = x, p[1] = y"""
if indice == 0:
return self.x
elif indice == 1:
return self.y
raise IndexError("Índice fuera de rango (0 o 1)")
def distancia_origen(self):
"""Distancia al origen (0,0)"""
return (self.x ** 2 + self.y ** 2) ** 0.5En una clase de Python hay muchos métodos “dunder” (o mágicos), que son convenciones que Python usa para llamar a métodos especiales y atributos
__init__(self, ...): constructor; inicializa el estado del objeto al crearlo.__del__(self) (poco recomendable en la práctica): se ejecuta justo antes de que el objeto sea destruido, normalmente no se usa salvo casos muy específicos.__repr__(self): representación “oficial”, pensada para debug y para que, si es posible, sea un string que permita reconstruir el objeto (Point2D(1, 2)). Se usa en repr(x) y en muchas shells interactivas.__str__(self): representación “amigable” para usuarios finales; se usa en str(x) y print(x) cuando está definido, y normalmente es más legible que __repr__.__eq__(self, other): define igualdad (==).__lt__(self, other): “less than”, define < (orden estricto).Opcionalmente, según necesidad: __le__ (\(\le\)), __gt__ (\(\gt\)), __ge__ (\(\ge\)), __ne__ (\(\ne\)).
Muchas veces se implementa solo un subconjunto y los otros se infieren para completar el resto.
__len__(self): permite usar len(obj) (colecciones, contenedores, etc.).__bool__(self): define la verdad lógica del objeto en contextos booleanos (if obj:), si no existe se usa __len__ por defecto.Para tipos numéricos o “like-number”:
__add__, __sub__, __mul__, etc.: sobrecarga de los operadores suma (\(+\)), resta (\(-\)), multiplicación (\(·\)) cuando tiene sentido para el modelo.__truediv__ (\(/\)), o los “in place” __iadd__ (+=), __isub__ (-=).Para tipos que representan colecciones:
__getitem__(self, key): acceso obj[key] (índices o claves).__setitem__(self, key, value): asignación obj[key] = value.__iter__(self): hace que el objeto sea iterable en for, list(obj), etc.# Crear puntos
p1 = Punto2D(3, 4)
p2 = Punto2D(3, 4)
p3 = Punto2D(1, 2)
# Representaciones
print(repr(p1)) # Punto2D(3.0, 4.0)
print(str(p1)) # (3.0, 4.0)
print(p1) # (3.0, 4.0)
# Comparaciones
print(p1 == p2) # True
print(p1 < p3) # False
# Operaciones
print(p1 + p3) # Punto2D(4.0, 6.0)
print(p1 * 2) # Punto2D(6.0, 8.0)
# Colección-like
print(len(p1)) # 2
print(p1[0]) # 3.0
print(bool(p1)) # True
print(bool(Punto2D())) # False
print(p1.distancia_origen()) # 5.0Las librerías externas amplían las capacidades del lenguaje para dominios especializados como análisis de datos, calculo matricial y modelado financiero. Requieren instalación previa con pip (gestor de paquetes):
python -m pip install numpy # Computación numérica
python -m pip install pandas # Análisis de datos
python -m pip install matplotlib # Visualización
python -m pip install QuantLib # Finanzas cuantitativasNumPy proporciona arrays multidimensionales y operaciones matemáticas vectorizadas para cálculos numéricos de mayor rendimiento, evitando los loops de Python que son maslentos. Soporta álgebra lineal, estadística descriptiva y generación de números aleatorios, siendo la base de casi todas las librerías científicas en Python.
Su estructura fundamental no es una lista de Python, sino el ndarray (N-dimensional array). A diferencia de las listas tradicionales que son contenedores flexibles de punteros a objetos dispersos en memoria, un ndarray es un bloque de memoria contiguo que contiene elementos de un solo tipo de dato (homogéneos):
Informacion: https://numpy.org/doc/2.4/
Pandas ofrece estructuras de datos como DataFrames (tablas) y Series (vectores), con herramientas para lectura/escritura de ficheros (CSV, Excel, SQL), limpieza de datos, transformaciones, agrupaciones y análisis exploratorio.
Es la biblioteca de referencia en el ecosistema de Python para la manipulación y análisis de datos estructurados.
El núcleo de Pandas se basa en dos estructuras de datos principales: las Series (unidimensionales, como una lista o una columna) y los DataFrames (bidimensionales, como una tabla).
Informacion: https://pandas.pydata.org/docs/
SciPy es una librería de cómputo científico construida sobre NumPy, que añade módulos de alto nivel para optimización, integración numérica, estadística avanzada, procesamiento de señales, álgebra lineal y más. Es especialmente útil en contextos de ingeniería y ciencia aplicada, donde se necesitan algoritmos numéricos robustos como métodos de optimización, resolución de ecuaciones diferenciales o ajustes de modelos.
Informacion: https://docs.scipy.org/doc/scipy/
Matplotlib genera gráficos (líneas, barras, scatter, histogramas)** con control sobre estilos, colores, etiquetas y anotaciones, permite crear visualizaciones estáticas interactivas para reportes, presentaciones y análisis exploratorio de datos.
Es la librería fundamental y más antigua para la visualización de datos en Python. Fue diseñada originalmente para imitar el comportamiento de graficado de MATLAB, ofreciendo a los científicos e ingenieros un control granular sobre cada elemento visual de una figura.
Información: https://matplotlib.org/
QuantLib es una librería especializada en valoración de derivados, calibración de curvas de tipos de interés, cálculo de volatilidad, análisis de riesgos y simulación de Monte Carlo.
Informacion: https://quantlib-python-docs.readthedocs.io/en/latest/
import numpy as np
# Array 1D (vector)
array_1d = np.array([1, 2, 3, 4, 5])
# Array 2D (matriz)
array_2d = np.array([[1, 2, 3],
[4, 5, 6]])
# Array 3D
array_3d = np.array([[[1, 2], [3, 4]],
[[5, 6], [7, 8]]])
# Array de ceros
zeros = np.zeros((3, 4)) # 3 filas, 4 columnas
# Array de unos
ones = np.ones((2, 3))
# Array con un valor específico
full = np.full((2, 2), 7) # Matriz 2x2 llena de 7s
# Array identidad (matriz diagonal con 1s)
identity = np.eye(3)
array_3d.shape # Dimensiones
array_3d.dtype # Tipo de dato
array_3d.size # Total de elementos
# Indexación y slicing
array_1d[0] # Primer elemento
array_1d[-1] # Ultimo elemento
array_1d[1:4] # Elementos 1 a 3
array_1d[::2] # Cada 2 elementos
array_2d[0, 1] # Fila 0, columna 1
array_2d.reshape(3, 2) # Redimensionar
array_2d.T # TranspuestaCreación eficiente de arrays: np.zeros, np.ones(), np.full() y np.eye()
Metadatos: .shape, .dtype y .size
Indexación y slicing
Manipulación de la estructura .T y .reshape()
Operaciones aritméticas Vectorizadas (Element-wise), NumPy aplica las operación a todo el bloque de datos simultáneamente.
Filtros (Boolean Masking) para filtrar datos sin usar condicionales if.
Manipulación de Formas (Concatenate & Stack) unir arrays, esencial
Concatenate: Une vectores uno detrás de otro (crece en longitud).
Stacking (Apilado):
import numpy as np
a = np.array([1, 2, 3, 4])
b = np.array([5, 6, 7, 8])
a + b # Suma
a - b # Resta
a * b # Multiplicación
b / a # División
a ** 2 # Potencia
np.sqrt(a)) # Raíz cuadrada
array = np.array([10, 20, 30, 40, 50, 60])
mask = array > 30 # elementos que cumplen la condición)
resultado = array[mask] # Aplicar la máscara para obtener elementos
array[array > 30] # [40 50 60]
array[(array > 20) & (array < 50)] # Múltiples condiciones
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
concat = np.concatenate([a, b]) # Concatenar (juntar end-to-end)
concat # [1 2 3 4 5 6]
# Stack (apilar verticalmente)
m1 = np.array([[1, 2], [3, 4]])
m2 = np.array([[5, 6], [7, 8]])
vstacked = np.vstack([m1, m2]) # Apilar filas
hstacked = np.hstack([m1, m2]) # Apilar columnasimport numpy as np
data = np.array([10, 20, 30, 40, 50])
np.mean(data) # Media (promedio)
np.median(data) # Mediana
np.std(data) # Desviación estándar
np.var(data) # Varianza
np.min(data) # Mínimo y máximo
np.max(data)
np.sum(data) # Suma y producto
np.prod(data)
np.percentile(data, 25) # Percentil
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
producto = np.dot(a, b) # O: a @ b
transpuesta = a.T # Transpuesta
det = np.linalg.det(a) # Determinante
inversa = np.linalg.inv(a) # Matriz inversa
# Numeros pseudo aleatorios
# Establecer semilla
np.random.seed(42)
# Uniformes [0, 1)
random_uniform = np.random.rand(5)
# Normales estandar(distribución Gaussiana)
random_normal = np.random.randn(5)
# Enteros aleatorios en rango [0, 10)
random_int = np.random.randint(0, 10, 5)
# Array aleatorio 3x3
random_matrix = np.random.rand(3, 3)Estadística básica en arrays: np.mean, np.median, np.std, np.var, np.min, np.max, np.sum, np.prod y np.percentile.
Álgebra lineal: producto matricial np.dot(a, b), transpuesta a.T, determinante np.linalg.det(a)e inversa np.linalg.inv(a).
Numeros pseudoaleatorios: fijar semillas np.random.seed(42), simulacion de números aleatorios uniformes np.random.rand(5), normales estándar con np.random.randn(5), enteros con np.random.randint(0, 10, 5) y una matriz aleatoria 3x3 con np.random.rand(3, 3)
import pandas as pd
serie1 = pd.Series([10, 20, 30, 40])
serie2 = pd.Series([10, 20, 30, 40],
index=['A', 'B', 'C', 'D'])
serie2['B'] # 20 - Acceso por etiqueta
serie2['A'] # 10
# Crear serie desde diccionario
datos = {'España': 47, 'Francia': 67, 'Alemania': 83, 'Italia': 59}
serie_paises = pd.Series(datos)
# Atributos de pd.Serie
serie_paises.size
serie_paises.index
serie_paises.dtype
serie_paises.values
# Dataframe
# Desde diccionario donde cada clave es una columna
datos = {
'Nombre': ['Alice', 'Bob', 'Carlos', 'Diana'],
'Edad': [25, 30, 28, 35],
'Salario': [50000, 60000, 55000, 70000],
'Departamento': ['Ventas', 'IT', 'Finanzas', 'IT']
}
# Desde lista de datos
datos = [
['Alice', 25, 50000],
['Bob', 30, 60000],
['Carlos', 28, 55000]
]
df = pd.DataFrame(datos,
columns=['Nombre', 'Edad', 'Salario'])
df.shape
df.columns # Nombre de columnas
df.index # RangeIndex(start=0, stop=3, step=1)
df.dtypes
df.info() # Tipo de dato, valores no-nulos
df.head() # Por defecto, primeras 5 filas
df.head(2) # Primeras 2 filas
df.tail() # Últimas 5 filasLa Serie (pd.Series): Un Array con Etiquetas. A diferencia de un array de NumPy (que solo tiene posiciones numéricas 0, 1, 2…), la Serie permite definir un índice explícito (etiquetas como ‘A’, ‘B’, ‘España’).
El DataFrame (pd.DataFrame): Es una colección de Series alineadas (comparten el mismo índice), formando una tabla de filas y columnas. Se construye desde:
.head() / .tail(): Muestra las primeras/últimas filas..info(): Muestra tipos de datos, uso de memoria y conteo de nulos..shape y .columns: Dimensiones (filas, columnas) y nombres de variables..loc (Label-based): Busca por etiqueta/nombre. df.loc[0, 'Nombre'] significa “Fila con índice 0 y Columna llamada ‘Nombre’”..iloc (Integer-based): Busca por posición numérica (como NumPy). df.iloc[0] es “La primera fila”, independientemente de cómo se llame su índice.Filtrado Condicional (Queries): extraer subconjuntos de datos. Condiciones Lógicas: df[df['Edad'] > 28] devuelve solo las filas que cumplen la condición. Operadores Combinados: Uso de & (AND) y | (OR) para lógica compleja. Filtros de Texto: Métodos vectorizados de strings como .str.contains('li') o .isin([...]).
Transformación y Edición: nuevas columnas 2026 - df['Edad'] o un valor constante, .drop(..., axis=1): Elimina columnas. .rename(): renombra variables para mayor claridad. .sort_values(): Ordena los datos soporta múltiples niveles de ordenación.
df = pd.DataFrame({
'Nombre': ['Alice', 'Bob', 'Carlos'],
'Edad': [25, 30, 28],
'Salario': [50000, 60000, 55000]
})
df.Nombre # Columnas
df['Edad']
df[['Nombre', 'Salario']] # Múltiples columnas
df.loc[0] # Filas
df.loc[0:2]
df.loc[0, 'Nombre'] # Elemento
df.iloc[0] # Por posición (.iloc[])
df.iloc[0:2]
df.iloc[-2:]
# Filtros
df[df['Edad'] > 28]
df[(df['Edad'] > 27) & (df['Salario'] > 55000)]
df[df['Nombre'].isin(['Alice', 'Bob'])]
df[df['Nombre'].str.contains('li')]
# Crear nuevas columnas
df['Fecha Nacimiento'] = 2026 - df['Edad']
df['País'] = 'España'
# Eliminar una columna
df = df.drop('País', axis=1)
df = df.rename(columns={'Edad': 'Años', 'Salario': 'Sueldo'})
df.sort_values(by='Edad')
df.sort_values(by=['Edad', 'Salario'])
df.sort_index()df = pd.DataFrame({
'Nombre': ['Alice', 'Bob', np.nan, 'Diana'],
'Edad': [25, np.nan, 28, 35],
'Salario': [50000, 60000, 55000, np.nan]
})
# Detectar valores faltantes
print(df.isna()) # Matriz booleana
print(df.isnull()) # Alias para isna()
# Contar valores faltantes
print(df.isna().sum()) # Por columna
# Output:
# Nombre 1
# Edad 1
# Salario 1
# Eliminar filas con valores faltantes
df_limpio = df.dropna()
print(df_limpio)
# Rellenar valores faltantes
df_relleno = df.fillna(0) # Rellenar con 0
df_relleno = df.fillna(df.mean()) # Rellenar con la media
# Rellenar hacia adelante (forward fill)
df_ffill = df.fillna(method='ffill')
# Rellenar hacia atrás (backward fill)
df_bfill = df.fillna(method='bfill')
df = pd.DataFrame({
'Edad': [25, 30, 28, 35, 32],
'Salario': [50000, 60000, 55000, 70000, 62000]
})
# Estadísticas para una columna
print(df['Edad'].mean()) # Media: 30.0
print(df['Edad'].median()) # Mediana: 30
print(df['Edad'].std()) # Desviación estándar
print(df['Edad'].var()) # Varianza
print(df['Edad'].min()) # Mínimo: 25
print(df['Edad'].max()) # Máximo: 35
# Resumen estadístico completo
print(df.describe())
# Contar valores únicos
print(df['Edad'].nunique()) # Número de valores únicos
# Obtener valores únicos
print(df['Edad'].unique()) # [25 30 28 35 32]
# Contar ocurrencias
print(df['Edad'].value_counts())Gestión de Datos Faltantes: df.isna().sum() ver rápidamente la calidad de tus datos. dropna borrar filas incompletas. fillna: rellenar los huecos para no perder información.
Estadística Descriptiva: Cálculo directo de mean (promedio), median (mediana), std (volatilidad) ignorando automáticamente los nulos. describe(): información estadistica del DataFrame (cuartiles, extremos, media).
Análisis de Frecuencias: value_counts() y nunique() ver qué valores son los más comunes o repetidos.
Agregación (groupby): Creación de grupos, para aplicar métricas .agg(['mean', 'min', 'max']) calcula varios estadisticas para cada grupo,
Concatenación (concat): unir pd.DataFrames
df = pd.DataFrame({
'Departamento': ['Ventas', 'IT', 'Finanzas', 'Ventas', 'IT', 'Finanzas'],
'Empleado': ['Alice', 'Bob', 'Carlos', 'Diana', 'Eve', 'Frank'],
'Salario': [50000, 60000, 55000, 52000, 65000, 58000]
})
# Agrupar por departamento
grupo = df.groupby('Departamento')
# Calcular la media salarial por departamento
print(grupo['Salario'].mean())
print(grupo['Salario'].agg(['mean', 'min', 'max', 'count']))
print(grupo[['Salario']].describe())
print(grupo.size())
df1 = pd.DataFrame({
'Nombre': ['Alice', 'Bob'],
'Edad': [25, 30]
})
df2 = pd.DataFrame({
'Nombre': ['Carlos', 'Diana'],
'Edad': [28, 35]
})
# Concatenar verticalmente (apilar filas)
df_concat = pd.concat([df1, df2], ignore_index=True)
# Concatenar horizontalmente (apilar columnas)
df3 = pd.DataFrame({
'Salario': [50000, 60000, 55000, 70000]
})
df_concat_h = pd.concat([df_concat, df3], axis=1)
# Tabla de empleados
empleados = pd.DataFrame({
'ID': [1, 2, 3],
'Nombre': ['Alice', 'Bob', 'Carlos'],
'Departamento': ['Ventas', 'IT', 'Finanzas']
})
# Tabla de salarios
salarios = pd.DataFrame({
'ID': [1, 2, 3],
'Salario': [50000, 60000, 55000]
})
# Merge interno (INNER JOIN)
df_merged = pd.merge(empleados, salarios, on='ID')
print(df_merged)
# Merge izquierdo (LEFT JOIN)
df_left = pd.merge(empleados, salarios, on='ID', how='left')
# Merge derecho (RIGHT JOIN)
df_right = pd.merge(empleados, salarios, on='ID', how='right')
# Merge externo (OUTER JOIN)
df_outer = pd.merge(empleados, salarios, on='ID', how='outer')import pandas as pd
# Leer un CSV
df = pd.read_csv('datos.csv')
# Guardar a CSV
df.to_csv('datos_procesados.csv', index=False)
# Parámetros útiles
df = pd.read_csv('datos.csv',
sep=';', # Separador (por defecto ',')
encoding='utf-8', # Codificación
nrows=1000) # Leer solo primeras 1000 filas
# Leer Excel
df = pd.read_excel('datos.xlsx', sheet_name=0)
# Guardar a Excel
df.to_excel('datos.xlsx', index=False)
# Escribir múltiples hojas
with pd.ExcelWriter('datos.xlsx') as writer:
df1.to_excel(writer, sheet_name='Hoja1', index=False)
df2.to_excel(writer, sheet_name='Hoja2', index=False)
# Leer JSON
df = pd.read_json('datos.json')
# Guardar a JSON
df.to_json('datos.json', orient='records')
df = pd.DataFrame({
'Nombre': ['Alice', 'Bob', 'Carlos'],
'Salario': [50000, 60000, 55000]
})
# Función personalizada
def categorizar_salario(salario):
if salario < 52000:
return 'Bajo'
elif salario < 58000:
return 'Medio'
else:
return 'Alto'
# Aplicar a una columna
df['Categoria'] = df['Salario'].apply(categorizar_salario)
# Usar lambda (funciones anónimas)
df['Salario_Ajustado'] = df['Salario'].apply(lambda x: x * 1.1) # Aumentar 10%
# Aplicar a toda la fila
df['Nombre_Largo'] = df.apply(lambda row: row['Nombre'] + ' (' + str(row['Salario']) + ')', axis=1)
df = pd.DataFrame({
'Mes': ['Enero', 'Enero', 'Febrero', 'Febrero'],
'Región': ['Norte', 'Sur', 'Norte', 'Sur'],
'Ventas': [1000, 1500, 1200, 1800]
})
# Tabla dinámica
pivot = df.pivot_table(values='Ventas',
index='Mes',
columns='Región',
aggfunc='sum')Input/Output Pandas es capaz de leer y escribir en los formatos más comunes de la industria: archivos de texto plano (CSV, JSON) hasta hojas de cálculo (Excel). Permite ajustar la lectura al archivo: definir separadores (sep=';'), o cargar solo una muestra (nrows=1000) para archivos grances.
Aplicación de funciones (apply) si las funciones predefinidas no son suficientes, apply permte aplicar funciones definidas por el usuario. Permite operar celda a celda o fila a fila (axis=1).
Pivot Tables tablas finámicas como en excel
import QuantLib as ql
today = ql.Date(15, 6, 2020) # Día, Mes, Año
# Establecer como fecha de evaluación global
ql.Settings.instance().evaluationDate = today
print(ql.Settings.instance().evaluationDate) # June 15th, 2020
# Sumar días
future_date = today + 30 # 30 días después
# Sumar períodos
six_months = today + ql.Period(6, ql.Months)
# Diferencia entre fechas
days_diff = (six_months - today)
# Calendario: festivos de Estados Unidos
calendar = ql.UnitedStates(ql.UnitedStates.NYSE)
# Contador de días: Actual/360 (mercados monetarios)
dayCounter = ql.Actual360()
# Contador de días: Actual/365 (bonos, derivados)
dayCounter_365 = ql.Actual365Fixed()
# Contador de días: 30/360 (mercados corporativos)
dayCounter_30_360 = ql.Thirty360()
# Contador de días: 30/360 (mercados corporativos)
dayCounter_bus_252 = ql.Business252(calendar)
# Calcular fracción de año
start_date = ql.Date(1, 1, 2020)
end_date = ql.Date(30, 6, 2020)
(end_date - start_date) / 360
year_fraction = dayCounter.yearFraction(start_date, end_date)
calendar.businessDaysBetween(start_date, end_date) / 252
dayCounter_bus_252.yearFraction(start_date, end_date)ql.Settings.instance().evaluationDate - fecha de referencia para descontar los flujos de caja futuros.
Aritmética de fecha con ql.Period()
Calendarios de Mercado los mercados no abren todos los días. Estos calendarios implemtan los festivos históricos y futuros de los principales meracados, como la bolsa de Nueva York, Londres, o TARGET en Europa. Y permiten calcular días hábiles (businessDaysBetween) entre dos fechas.
Convenciones de Conteo de Días (DayCounters) Define cómo se mide un año en diferentes mercados, lo que determina el cálculo de intereses (devengo):
Actual/360: Estándar en mercados monetarios.Actual/365: Usado en bonos gubernamentales y derivados.30/360: Estándar en bonos corporativos de EE.UU. (asume todos los meses de 30 días para simplificar pagos).Business/252: Usado en mercados emergentes como Brasil, donde solo cuentan los días hábiles (~252 al año).Una curva de tipos de interés (yield curve) es una función que muestra la relación entre el plazo de vencimiento y la tasa de interés de descuento. Es fundamental en finanzas porque permite:
Bootstrapping vs Fitting
Bootstrapping: Construye una curva que reprecia exactamente los instrumentos de mercado. Usa interpolación local (lineal, cúbica).
Fitting: Ajusta parámetros de un modelo paramétrico (Nelson-Siegel, Svensson) minimizando diferencias de precios. Produce curvas más suaves pero aproximadas.
import QuantLib as ql
today = ql.Date(15, 6, 2020)
ql.Settings.instance().evaluationDate = today
# Parámetros
settlement_days = 2
calendar = ql.UnitedStates(ql.UnitedStates.NYSE)
forward_rate = 0.05 # 5% de tasa de interés
day_counter = ql.Actual360()
# Crear curva plana
flat_forward = ql.FlatForward(settlement_days, calendar,
forward_rate, day_counter)
flat_forward.referenceDate()
flat_forward.discount(today + ql.Period(1, ql.Years))
# Definir puntos de la curva
dates = [ql.Date(15, 6, 2020),
ql.Date(15, 12, 2020),
ql.Date(15, 6, 2021),
ql.Date(15, 6, 2022)]
yields = [0.01, 0.015, 0.02, 0.025] # Tasas en cada fecha
day_counter = ql.Actual360()
# Crear curva con interpolación lineal
curve = ql.LogLinearZeroCurve(dates, yields, day_counter)
# Handle para usar la curva
curve_handle = ql.YieldTermStructureHandle(curve)
# Obtener tasa forward a una fecha
target_date = ql.Date(15, 9, 2020)
forward_rate = curve_handle.forwardRate(target_date, target_date,
day_counter, ql.Simple)
forward_rate.rate()Agenda:
Introducción a la programación en Python
Python
Valoración de derivados financieros con Python
Modelo multifactorial de riesgo de crédito
Imaginemos que queremos valorar y estudiar los riesgos asociados a un contrato financiero cuyo valor final depende del resultado del lanzamiento de la moneda (\(\omega\)) pagamos un 1€ si sale cara y recibimos 1€ si sale cruz. Nosotros como creadores de mercado (market-makers) ofrecemos precios de compra o venta. El espacio muestral es \(\Omega = {H, T}\) (Cara, Cruz).
El primer paso es definir el Payoff (\(V\)), una variable aleatoria al final en \(t_1\):
El valor teórico del contrato en \(t_0\) (\(V_0\)) se define como la esperanza matemática de sus flujos de caja futuros, descontados a la tasa libre de riesgo (\(r\)).
\[ V_0=\frac{1}{1 + r} E[V]=\frac{1}{1 + r} \left[p(H) · V(H) + p(T) · V(C) \right] = \frac{1}{1 + r} \left[p(H) · (-1) + p(T) · (+1) \right] = \frac{p(T) - p(H)}{(1 + r)} = 0 \]
¿Todo lo que me paguen por encima de su valor teórico es beneficio? ¿Cuales son los riesgos asociados a este contrato?
Agenda:
Introducción a la programación en Python
Python
Valoración de derivados financieros con Python
Modelo multifactorial de riesgo de crédito
Laboratorio de Riesgos: Fundamentos de Python en el ámbito financiero